一个叫木头,一个叫马尾

React useEffect:每个开发者都应该知道的4个小技巧

文章译自 React useEffect: 4 Tips Every Developer Should Know[1] ,作者为 Helder Esteves

说是小技巧,其实不小。


我们谈谈 React Hooks 中的useEffects。 我将与你分享使用useEffect时应该注意的4个技巧。

一个useEffect只应该用于一个目的

React Hooks 中,你可以使用多次useEffect函数。这是一个很好的特性,因为,要编写干净的代码,一个函数只服务于一个目的是必要的(就像一句话应该只传达一个想法一样)。

译者注,这在软件开发中,也叫单一职责原则,The single responsibility principle (SRP)。

useEffects拆分成短小精炼的单用途函数,也可以防止一些意外的执行(在使用依赖数组时)。

例如,我们假设你有一个与变量varB无关的varA,你想建立一个基于useEffect的递归计数器(借助setTimeout)。【坏代码】像下面这样:

function App({
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);
  // 不要这么做!
  useEffect(() => {
    const timeoutA = setTimeout(() => setVarA(varA + 1), 1000);
    const timeoutB = setTimeout(() => setVarB(varB + 2), 2000);

    return () => {
      clearTimeout(timeoutA);
      clearTimeout(timeoutB);
    };
  }, [varA, varB]);

  return (
    <span>
      Var A: {varA}, Var B: {varB}
    </span>

  );
}

可以看到,变量varAvarB任意一个发生变化都会触发两个变量的更新。这就是为什么这个钩子不能正常工作。

由于这是一个很短的例子,你可能会觉得问题很好发现,然而,当函数更长时,伴随更多的代码和变量,再出现问题时就会让你头大了。所以,做正确的事情,按用途来拆分你的useEffect

对于上面的情况,应该这样处理:

function App({
  const [varA, setVarA] = useState(0);
  const [varB, setVarB] = useState(0);
  // 正确的方式
  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]);

  useEffect(() => {
    const timeout = setTimeout(() => setVarB(varB + 2), 2000);

    return () => clearTimeout(timeout);
  }, [varB]);

  return (
    <span>
      Var A: {varA}, Var B: {varB}
    </span>

  );
}

注意:这里的代码只是为了举例说明,目的是帮助你轻松理解useEffect的问题。通常情况下,当一个变量依赖于它之前的状态时,推荐的方法是用setVarA(varA => varA + 1)来代替。(感谢 @Michael Landis 的提醒)

尽可能使用自定义钩子

我们再来看看上面的例子。如果变量varAvarB是完全独立的呢?

在这种情况下,我们可以简单地创建一个自定义钩子来隔离每个变量。这样,你就可以准确地知道每个函数对哪个变量做了什么。

自定义钩子的代码:

function App({
  const [varA, setVarA] = useVarA();
  const [varB, setVarB] = useVarB();

  return (
    <span>
      Var A: {varA}, Var B: {varB}
    </span>

  );
}

function useVarA({
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]);

  return [varA, setVarA];
}

function useVarB({
  const [varB, setVarB] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarB(varB + 2), 2000);

    return () => clearTimeout(timeout);
  }, [varB]);

  return [varB, setVarB];
}

现在每个变量都有了自己的钩子。更易于维护和阅读了。

正确地运行带有条件的useEffect

继续以setTimeout为话题,我们给出下面的例子:

function App({
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]);

  return <span>Var A: {varA}</span>;
}

出于某种原因,你想计数到5就停止。有正确的实现方法,也有不正确的方法。

我们先来看看不正确的方法:

function App({
  const [varA, setVarA] = useState(0);

  // 不要这么做!
  useEffect(() => {
    let timeout;
    if (varA < 5) {
      timeout = setTimeout(() => setVarA(varA + 1), 1000);
    }

    return () => clearTimeout(timeout);
  }, [varA]);

  return <span>Var A: {varA}</span>;
}

虽然这样做是可行的,但请记住,clearTimeout将在varA发生任何变化时都会运行,而setTimeout则是有条件地运行(小于5时才会运行)。

对于条件化运行useEffect,推荐的方式是在函数的开头做一个条件返回,像这样:

 function App({
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    if (varA >= 5return;

    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]);

  return <span>Var A: {varA}</span>;
}

Material UI[2]中我们可以看到这种用法(以及许多其他的框架中),它确保你没有错误地运行useEffect

译者注:仔细比较上面2段代码,我们要达到的目的其实就是,要让setTimeoutclearTimeout成对的出现。而上面不对的代码中,可能会出现if语句把setTimeout拦截了但clearTimeout仍然出现的情况。所以第二种代码风格(提前return),是更好的条件化运行useEffect的方式。

useEffect中用到的属性都要在依赖数组中声明

如果你正在使用ESLint,那么你可能已经看到过一个来自ESLint exhaustive-deps[3]规则的警告。

这一点至关重要。当你的应用程序越来越大时,更多的依赖关系(属性)会被添加到相关的useEffect中。为了跟踪所有这些依赖,并避免过期的状态(stale closures),你应该将每一个依赖添加到依赖数组中。(这里有官方对这个问题的看法[4])

同样关于setTimeout的话题,假设你想只运行一次setTimeout,并给varA加1,就像前面的例子一样。

可能你尝试的是下面这样的代码:

 function App({
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, []); // 小心了: varA 没放在依赖数组中!

  return <span>Var A: {varA}</span>;
}

虽然这样做也能达到你的目的,但我们还是要花点时间想一想,"如果代码量变大了怎么办?",或者,"如果我想把上面的代码改成其他的东西怎么办?"

在这种情况下,你会希望把所有的依赖变量都加入进来,因为这将更容易测试和检测可能出现的问题(像过期的属性,stale props and closures)。

所以正确的方式是:

function App({
  const [varA, setVarA] = useState(0);

  useEffect(() => {
    if (varA > 0return;

    const timeout = setTimeout(() => setVarA(varA + 1), 1000);

    return () => clearTimeout(timeout);
  }, [varA]); // 非常棒,依赖数组得到了正确设置

  return <span>Var A: {varA}</span>;
}

就这样了,各位。如果你有什么问题或建议,我会认真听。 在下面回复或评论吧!


译者注:

之前在某博客上看到过一个有关useEffect用法的示例图,非常简洁明了,这里分享给大家:

来源: https://daveceddia.com
来源: https://daveceddia.com

[1]

React useEffect: 4 Tips Every Developer Should Know: https://medium.com/swlh/useeffect-4-tips-every-developer-should-know-54b188b14d9c

[2]

Material UI: https://material-ui.com/

[3]

exhaustive-deps rule: https://www.npmjs.com/package/eslint-plugin-react-hooks

[4]

Can I skip an effect on updates?: https://reactjs.org/docs/hooks-faq.html#can-i-skip-an-effect-on-updates